#!/usr/bin/env python
# -*-coding:UTF-8-*-
# Author: chenzuoqing@126.com
# Create: 2018/8/8 17:53
# File  : server_info.py

"""
获取CMDB基础数据，参考ansible facts的获取方式
仅依赖自带包和/sbin/ip命令（应该都自带，若路径不一致可以更改ip_path）
大部分信息按读取系统文件的方式获取
python 2.6 2.7 3.6版本测试可以正常
"""

import os
import sys
import glob
import json
import errno
import struct
import socket
import platform
import subprocess


def byte2human(n):
    symbols = ('K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y')
    prefix = {}
    for i, s in enumerate(symbols):  # 各单位字节大小
        prefix[s] = 1 << (i + 1) * 10  # 左移计算
        # print(i, s, type(prefix[s]), prefix[s], )
    for s in reversed(symbols):  # 从后往前，若字节数大于当前单位，则使用该单位显示
        if n >= prefix[s]:
            value = float(n) / prefix[s]
            # return "{:.2f}{}".format(value, s)
            return "%.2f%s" % (value, s)
    # return "{}B".format(n)
    return "%sB" % n


def get_machine_info():
    if os.path.exists('/sys/devices/virtual/dmi/id/product_name'):
        dmi_file_dict = {  # 虚拟机SN为空
            # 'bios_date': '/sys/devices/virtual/dmi/id/bios_date',
            # 'bios_version': '/sys/devices/virtual/dmi/id/bios_version',
            # 'form_factor': '/sys/devices/virtual/dmi/id/chassis_type',
            'manufacturer': '/sys/devices/virtual/dmi/id/sys_vendor',
            'model_name': '/sys/devices/virtual/dmi/id/product_name',
            'uuid': '/sys/devices/virtual/dmi/id/product_uuid',
            'sn': '/sys/devices/virtual/dmi/id/product_serial',
        }
        machine = {}
        for (key, path) in dmi_file_dict.items():
            data = get_file_content(path)
            machine[key] = data
        if not machine['sn']:  # 虚拟机sn=uuid
            machine['sn'] = machine['uuid']
        return machine
    else:
        return ''


def get_partition_uuid(partname):
    """
    获取磁盘分区的uuid
    """
    try:
        uuids = os.listdir("/dev/disk/by-uuid")
    except OSError:
        return

    for uuid in uuids:
        dev = os.path.realpath("/dev/disk/by-uuid/" + uuid)
        if dev == ("/dev/" + partname):
            return uuid

    return None


def get_file_content(path, default=None, strip=True):
    """
    读取文件内容，原样返回，单行的文件可以传入默认值和strip标识去除首位空字符
    """
    data = default
    if os.path.exists(path) and os.access(path, os.R_OK):
        try:
            with open(path) as datafile:
                data = datafile.read()
                if strip:
                    data = data.strip()
                if len(data) == 0:
                    data = default
        except:
            pass
    return data


def get_cpu_info(cpufile='/proc/cpuinfo'):
    """
    拼接cpu型号和数量返回字符串
    """
    cpudict = {}
    content = get_file_content(cpufile)
    for line in content.splitlines():
        if not line:
            continue
        data = line.split(':', 1)
        key, value = data[0].strip(), data[1].strip()
        if key == 'model name':
            if value in cpudict.keys():
                cpudict[value] += 1
            else:
                cpudict[value] = 1
    string = ''
    for k, v in cpudict.items():
        if string:
            string += ' and ', k + ' * ' + str(v)
        else:
            string = k + ' * ' + str(v)
    return string


def get_mem_info(memfile='/proc/meminfo'):
    """
    /proc/meminfo文件默认是KB，转成byte再换单位返回
    """
    content = get_file_content(memfile)
    for line in content.splitlines():
        if not line:
            continue
        data = line.split(':', 1)
        key, value = data[0].strip(), data[1].strip()
        if key == 'MemTotal':
            return byte2human(int(value.split()[0]) * 1024)
    return "0"


def get_disk_info():
    try:
        block_devs = os.listdir("/sys/block")
    except OSError:
        # pass
        return "get_disk_info执行出错."

    space_sum = 0
    for block in block_devs:
        fixed_device = 0
        virtual = 1
        sysfs_no_links = 0
        try:
            path = os.readlink(os.path.join("/sys/block/", block))
        except OSError:
            e = sys.exc_info()[1]
            if e.errno == errno.EINVAL:
                path = block
                sysfs_no_links = 1
            else:
                continue
        if "virtual" in path:
            continue
        sysdir = os.path.join("/sys/block", path)
        if sysfs_no_links == 1:
            for folder in os.listdir(sysdir):
                if "device" in folder:
                    virtual = 0
                    break
            if virtual:
                continue
        d = {}
        # diskname = os.path.basename(sysdir)
        # 磁盘详细信息
        # for key in ['vendor', 'model', 'sas_address', 'sas_device_handle']:
        #    d[key] = get_file_content(sysdir + "/device/" + key)

        # 可拆卸设备
        for key, test in [('removable', '/removable'),
                          ('support_discard', '/queue/discard_granularity'),
                          ]:
            value = get_file_content(sysdir + test)
            if value != "0":  # 只读取硬盘，便携设备不读取
                fixed_device = 1
                break
            # print(diskname, key, test)
            # d[key] = value
        if fixed_device != 0:
            continue

        # 具体分区的信息
        # print(sysdir)
        # #扇区数
        # d['sectors'] = get_file_content(sysdir + "/size")
        # if not d['sectors']:
        #     d['sectors'] = 0
        # # 扇区大小
        # d['sectorsize'] = get_file_content(sysdir + "/queue/logical_block_size")
        # if not d['sectorsize']:
        #     d['sectorsize'] = get_file_content(sysdir + "/queue/hw_sector_size",512)

        # 只计算取磁盘大小
        sectors = get_file_content(sysdir + "/size")
        sectorsize = get_file_content(sysdir + "/queue/logical_block_size")
        if not sectorsize:
            sectorsize = get_file_content(sysdir + "/queue/hw_sector_size", 512)

        # 转换字节成数值最大单位
        # d['size'] = byte2human(int(sectors) * int(sectorsize))
        d['size'] = int(sectors) * int(sectorsize)
        space_sum += d['size']

        # 遍历取每个分区的信息
        # d['partitions'] = {}
        # for folder in os.listdir(sysdir):
        #     m = re.search("(" + diskname + "\d+)", folder)
        #     if m:
        #         part = {}
        #         partname = m.group(1)
        #         part_sysdir = sysdir + "/" + partname

        #         part['start'] = get_file_content(part_sysdir + "/start",0)
        #         part['sectors'] = get_file_content(part_sysdir + "/size",0)
        #         part['sectorsize'] = get_file_content(part_sysdir + "/queue/logical_block_size")
        #         if not part['sectorsize']:
        #             part['sectorsize'] = get_file_content(part_sysdir + "/queue/hw_sector_size",512)
        #         part['size'] = float(part['sectors']) * float(part['sectorsize'])
        #         part['uuid'] = get_partition_uuid(partname)
        #         d['partitions'][partname] = part
        # print({diskname: d})
    return byte2human(space_sum)


def get_default_interfaces(ip_path):
    """
    判断默认出口的IP地址，做了IPv6，但是没用它
    """
    command = dict(
        v4=[ip_path, '-4', 'route', 'get', '8.8.8.8'],
        v6=[ip_path, '-6', 'route', 'get', '2404:6800:400a:800::1012']
    )
    interface = dict(v4={}, v6={})
    for v in 'v4', 'v6':
        if v == 'v6':
            continue
        out = run_command(command[v])
        if not out:
            continue
        words = out.splitlines()[0].split()
        if len(words) > 0 and words[0] == command[v][-1]:
            for i in range(len(words) - 1):
                if words[i] == 'dev':
                    interface[v]['interface'] = words[i + 1]
                elif words[i] == 'src':
                    interface[v]['address'] = words[i + 1]
                elif words[i] == 'via' and words[i + 1] != command[v][-1]:
                    interface[v]['gateway'] = words[i + 1]
    return interface['v4'], interface['v6']


def run_command(args, shell=False):
    """
    执行命令，返回输出
    """
    cmd = subprocess.Popen(args,
                           stdin=subprocess.PIPE, stdout=subprocess.PIPE,
                           stderr=subprocess.PIPE, universal_newlines=True,
                           shell=shell)
    stdout = cmd.stdout.read()
    # stderr = cmd.stderr.read()
    return stdout


def get_interfaces_info(ip_path):
    """
    通过网卡文件获取接口信息，删减了很多，另外接口ip地址调用了系统的ip命令
    虚拟网卡会append到所属物理网卡ip列表中，算是一个网卡多个ip
    """
    interfaces = []

    for path in glob.glob('/sys/class/net/*'):
        if not os.path.isdir(path):
            continue
        device = os.path.basename(path)
        this = {'name': device}
        # mac address
        if os.path.exists(os.path.join(path, 'address')):
            # 设置默认值，防止插入数据失败
            macaddress = get_file_content(os.path.join(path, 'address'), default='')
            #this['mac_address'] = macaddress if macaddress else '00:00:00:00:00:00'
            if macaddress == '00:00:00:00:00:00' and device == 'lo':  # 不记录回环地址
                continue
        # is activate
        # if os.path.exists(os.path.join(path, 'operstate')):
        #     this['active'] = get_file_content(os.path.join(path, 'operstate')) != 'down'
        # # type
        # if os.path.exists(os.path.join(path, 'bridge')):
        #     this['type'] = 'bridge'
        # if os.path.exists(os.path.join(path, 'bonding')):
        #     this['type'] = 'bonding'

        ips = []

        # def parse_ip_output(output, secondary=False):
        def parse_ip_output(output):
            for line in output.splitlines():
                if not line:
                    continue
                words = line.split()
                # broadcast = ''
                if words[0] == 'inet':
                    if '/' in words[1]:
                        address, netmask_length = words[1].split('/')
                        # if len(words) > 3:
                        #     broadcast = words[3]
                    else:
                        address = words[1]
                        netmask_length = "32"
                    # address_bin = struct.unpack('!L', socket.inet_aton(address))[0]
                    netmask_bin = (1 << 32) - (1 << 32 >> int(netmask_length))
                    netmask = socket.inet_ntoa(struct.pack('!L', netmask_bin))
                    # network = socket.inet_ntoa(struct.pack('!L', address_bin & netmask_bin))
                    # iface = words[-1]
                    ips.append({'ip_addr': address, 'netmask': netmask})
                this['ips'] = ips
                if this not in interfaces:
                    interfaces.append(this)

        args = [ip_path, 'addr', 'show', 'primary', device]
        primary_data = run_command(args=args)
        args = [ip_path, 'addr', 'show', 'secondary', device]
        secondary_data = run_command(args)

        parse_ip_output(primary_data)
        parse_ip_output(secondary_data)

    return interfaces


def populate():
    ip_path = "/sbin/ip"
    if ip_path is None:
        return None, None, None
    default_ipv4, default_ipv6 = get_default_interfaces(ip_path)
    interfaces = get_interfaces_info(ip_path)
    return interfaces, default_ipv4, default_ipv6


def get_methods():
    try:
        from urllib2 import Request, urlopen
    except ImportError:
        from urllib.request import Request, urlopen
    return Request, urlopen


if __name__ == '__main__':
    network, ipv4, ipv6 = populate()
    info = {
        'ip': ipv4['address'],
        'hostname': platform.node(),
        # linux_distribution已经过期，将移除，Python 3.7及之后可以安装distro模块用distro.linux_distribution()替代它
        'os': ' '.join(platform.linux_distribution()).capitalize(),
        'cpu': get_cpu_info(),
        'mem': get_mem_info(),
        'disk': get_disk_info(),
        "network": network,
    }
    info.update(get_machine_info())
    # print(json.dumps(info, sort_keys=True, indent=4))

    headers = {'Content-Type': 'application/json'}
    url = 'http://ops.an00.cn/autoreport/'
    request, urlopen = get_methods()
    req = request(headers=headers, data=json.dumps(info).encode('utf-8'), url=url)
    try:
        res = urlopen(req)
    except Exception as e:
        print('提交数据出错，请确认接口权限或数据字段是否变更...')
        print(e)
    else:
        print(res)
